# -*- coding: utf-8 -*-
# @Time    : 2023/5/9 9:52
# @Author  : Pan
# @Software: PyCharm
# @Project : VisualFramework
# @FileName: Unet

import paddle
import paddle.nn as nn
import paddle.nn.functional as F


class EMA:
    def __init__(self, beta):
        super().__init__()
        self.beta = beta
        self.step = 0

    def update_model_average(self, ma_model, current_model):
        for current_params, ma_params in zip(current_model.parameters(), ma_model.parameters()):
            old_weight, up_weight = ma_params, current_params
            ma_params = self.update_average(old_weight, up_weight)

    def update_average(self, old, new):
        if old is None:
            return new
        return old * self.beta + (1 - self.beta) * new

    def step_ema(self, ema_model, model, step_start_ema=1000):
        if self.step < step_start_ema:
            self.reset_parameters(ema_model, model)
            self.step += 1
            return
        self.update_model_average(ema_model, model)
        self.step += 1

    def reset_parameters(self, ema_model, model):
        ema_model.set_state_dict(model.state_dict())


class SelfAttention(nn.Layer):
    """
    SelfAttention 将channels的信息做了融合
    """
    def __init__(self, channels, size):
        super(SelfAttention, self).__init__()
        self.channels = channels
        self.size = size
        self.mha = nn.MultiHeadAttention(channels, 4)
        self.ln = nn.LayerNorm([channels])
        self.ff_self = nn.Sequential(
            nn.LayerNorm([channels]),
            nn.Linear(channels, channels),
            nn.GELU(),
            nn.Linear(channels, channels),
        )

    def forward(self, x):
        x = x.reshape([-1, self.channels, self.size * self.size]).transpose([0, 2, 1])
        x_ln = self.ln(x)
        attention_value = self.mha(x_ln, x_ln)
        attention_value = attention_value + x
        attention_value = self.ff_self(attention_value) + attention_value
        x = attention_value.transpose([0, 2, 1]).reshape([-1, self.channels, self.size, self.size])
        return x


class DoubleConv(nn.Layer):
    def __init__(self, in_channels, out_channels, mid_channels=None, residual=False):
        super().__init__()
        self.residual = residual
        if not mid_channels:
            mid_channels = out_channels
        self.double_conv = nn.Sequential(
            nn.Conv2D(in_channels, mid_channels, kernel_size=3, padding=1, bias_attr=False),
            nn.GroupNorm(1, mid_channels),
            nn.GELU(),
            nn.Conv2D(mid_channels, out_channels, kernel_size=3, padding=1, bias_attr=False),
            nn.GroupNorm(1, out_channels),
        )

    def forward(self, x):
        if self.residual:
            return F.gelu(x + self.double_conv(x))
        else:
            return self.double_conv(x)


class Down(nn.Layer):
    def __init__(self, in_channels, out_channels, emb_dim=256):
        super().__init__()
        self.maxpool_conv = nn.Sequential(
            nn.MaxPool2D(2),
            DoubleConv(in_channels, in_channels, residual=True),
            DoubleConv(in_channels, out_channels),
        )

        self.emb_layer = nn.Sequential(
            nn.Silu(),
            nn.Linear(
                emb_dim,
                out_channels
            ),
        )

    def forward(self, x, t):
        x = self.maxpool_conv(x)
        emb = self.emb_layer(t)[:, :, None, None].tile([1, 1, x.shape[-2], x.shape[-1]])
        return x + emb


class Up(nn.Layer):
    def __init__(self, in_channels, out_channels, emb_dim=256):
        super().__init__()

        self.up = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=True)
        self.conv = nn.Sequential(
            DoubleConv(in_channels, in_channels, residual=True),
            DoubleConv(in_channels, out_channels, in_channels // 2),
        )

        self.emb_layer = nn.Sequential(
            nn.Silu(),
            nn.Linear(
                emb_dim,
                out_channels
            ),
        )

    def forward(self, x, skip_x, t):
        x = self.up(x)
        x = paddle.concat([skip_x, x], axis=1)
        x = self.conv(x)
        emb = self.emb_layer(t)[:, :, None, None].tile([1, 1, x.shape[-2], x.shape[-1]])
        return x + emb


class UNet(nn.Layer):
    def __init__(self, config):
        super().__init__()
        c_in = config["in_channels"] if "in_channels" in config.keys() else 3
        c_out = config["out_channels"] if "out_channels" in config.keys() else 3
        image_size = config["image_size"] if "image_size" in config.keys() else 96
        embed_dim = config["embed_dim"] if "embed_dim" in config.keys() else 16
        self.time_dim = config["time_dim"] if "time_dim" in config.keys() else 256
        self.inc = DoubleConv(c_in, embed_dim)
        self.down1 = Down(embed_dim, embed_dim * 2)
        self.sa1 = SelfAttention(embed_dim * 2, image_size // 2)
        self.down2 = Down(embed_dim * 2, embed_dim * 4)
        self.sa2 = SelfAttention(embed_dim * 4, image_size // 4)
        self.down3 = Down(embed_dim * 4, embed_dim * 4)
        self.sa3 = SelfAttention(embed_dim * 4, image_size // 8)

        self.bot1 = DoubleConv(embed_dim * 4, embed_dim * 8)
        self.bot2 = DoubleConv(embed_dim * 8, embed_dim * 8)
        self.bot3 = DoubleConv(embed_dim * 8, embed_dim * 4)

        self.up1 = Up(embed_dim * 8, embed_dim * 2)
        self.sa4 = SelfAttention(embed_dim * 2, image_size // 4)
        self.up2 = Up(embed_dim * 4, embed_dim)
        self.sa5 = SelfAttention(embed_dim, image_size // 2)
        self.up3 = Up(embed_dim * 2, embed_dim)
        self.sa6 = SelfAttention(embed_dim, image_size)
        self.outc = nn.Conv2D(embed_dim, c_out, kernel_size=1)

    def pos_encoding(self, t, channels):
        inv_freq = 1.0 / (
            10000
            ** (paddle.arange(0, channels, 2).astype(paddle.float32) / channels)
        )
        pos_enc_a = paddle.sin(t.tile([1, channels // 2]) * inv_freq)
        pos_enc_b = paddle.cos(t.tile([1, channels // 2]) * inv_freq)
        pos_enc = paddle.concat([pos_enc_a, pos_enc_b], axis=-1)
        return pos_enc

    def forward(self, x, t):
        t = t.unsqueeze(-1).astype(paddle.float32)
        t = self.pos_encoding(t, self.time_dim)

        x1 = self.inc(x)
        x2 = self.down1(x1, t)
        x2 = self.sa1(x2)
        x3 = self.down2(x2, t)
        x3 = self.sa2(x3)
        x4 = self.down3(x3, t)
        x4 = self.sa3(x4)

        x4 = self.bot1(x4)
        x4 = self.bot2(x4)
        x4 = self.bot3(x4)

        x = self.up1(x4, x3, t)
        x = self.sa4(x)
        x = self.up2(x, x2, t)
        x = self.sa5(x)
        x = self.up3(x, x1, t)
        x = self.sa6(x)
        output = self.outc(x)
        return output
